/* * Copyright 2014 Mario Guggenberger <mg@protyposis.net> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.protyposis.android.mediaplayer; import android.content.Context; import android.net.Uri; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.widget.MediaController; import android.widget.Toast; import java.io.IOException; import java.util.Map; /** * Created by maguggen on 04.06.2014. */ public class VideoView extends SurfaceView implements SurfaceHolder.Callback, MediaController.MediaPlayerControl { private static final String TAG = VideoView.class.getSimpleName(); private static final int STATE_ERROR = -1; private static final int STATE_IDLE = 0; private static final int STATE_PREPARING = 1; private static final int STATE_PREPARED = 2; private static final int STATE_PLAYING = 3; private static final int STATE_PAUSED = 4; private static final int STATE_PLAYBACK_COMPLETED = 5; private int mCurrentState = STATE_IDLE; private int mTargetState = STATE_IDLE; private MediaSource mSource; private int mVideoTrackIndex; private int mAudioTrackIndex; private MediaPlayer mPlayer; private SurfaceHolder mSurfaceHolder; private int mVideoWidth; private int mVideoHeight; private int mSeekWhenPrepared; private float mPlaybackSpeedWhenPrepared; private MediaPlayer.OnPreparedListener mOnPreparedListener; private MediaPlayer.OnSeekListener mOnSeekListener; private MediaPlayer.OnSeekCompleteListener mOnSeekCompleteListener; private MediaPlayer.OnCompletionListener mOnCompletionListener; private MediaPlayer.OnErrorListener mOnErrorListener; private MediaPlayer.OnInfoListener mOnInfoListener; private MediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener; public VideoView(Context context) { super(context); initVideoView(); } public VideoView(Context context, AttributeSet attrs) { super(context, attrs); initVideoView(); } public VideoView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initVideoView(); } private void initVideoView() { getHolder().addCallback(this); } /** * Sets a media source and track indices. See {@link MediaPlayer#setDataSource(MediaSource, int, int)} * for a detailed explanation of the parameters. * * @param source the media source * @param videoTrackIndex a video track index or one of the MediaPlayer#TRACK_INDEX_* constants * @param audioTrackIndex an video audio index or one of the MediaPlayer#TRACK_INDEX_* constants */ public void setVideoSource(MediaSource source, int videoTrackIndex, int audioTrackIndex) { mCurrentState = STATE_IDLE; mTargetState = STATE_IDLE; mSource = source; mVideoTrackIndex = videoTrackIndex; mAudioTrackIndex = audioTrackIndex; mSeekWhenPrepared = 0; mPlaybackSpeedWhenPrepared = 1; openVideo(); requestLayout(); invalidate(); } /** * Sets a media source. * @param source the media source */ public void setVideoSource(MediaSource source) { setVideoSource(source, MediaPlayer.TRACK_INDEX_AUTO, MediaPlayer.TRACK_INDEX_AUTO); } /** * @see android.widget.VideoView#setVideoPath(String) * @param path * @deprecated only for compatibility with Android API */ @Deprecated public void setVideoPath(String path) { setVideoSource(new UriSource(getContext(), Uri.parse(path))); } /** * @see android.widget.VideoView#setVideoURI(android.net.Uri) * @param uri * @deprecated only for compatibility with Android API */ @Deprecated public void setVideoURI(Uri uri) { setVideoSource(new UriSource(getContext(), uri)); } /** * @see android.widget.VideoView#setVideoURI(android.net.Uri, Map) * @param uri * @param headers * @deprecated only for compatibility with Android API */ @Deprecated public void setVideoURI(Uri uri, Map<String, String> headers) { setVideoSource(new UriSource(getContext(), uri, headers)); } public MediaPlayer getMediaPlayer() { // TODO do not return the real media player // Handling width it could result in invalid states, better return a "censored" wrapper interface return mPlayer; } private void openVideo() { if (mSource == null || mSurfaceHolder == null) { // not ready for playback yet, will be called again later return; } release(); mPlayer = new MediaPlayer(); mPlayer.setDisplay(mSurfaceHolder); mPlayer.setScreenOnWhilePlaying(true); mPlayer.setOnPreparedListener(mPreparedListener); mPlayer.setOnSeekListener(mSeekListener); mPlayer.setOnSeekCompleteListener(mSeekCompleteListener); mPlayer.setOnCompletionListener(mCompletionListener); mPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); mPlayer.setOnErrorListener(mErrorListener); mPlayer.setOnInfoListener(mInfoListener); mPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); // Create a handler for the error message in case an exceptions happens in the following thread final Handler exceptionHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { mCurrentState = STATE_ERROR; mTargetState = STATE_ERROR; mErrorListener.onError(mPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); return true; } }); // Set the data source asynchronously as this might take a while, e.g. is data has to be // requested from the network/internet. // IMPORTANT: // We use a Thread instead of an AsyncTask for performance reasons, because threads started // in an AsyncTask perform much worse, no matter the priority the Thread gets (unless the // AsyncTask's priority is elevated before creating the Thread). // See comment in MediaPlayer#prepareAsync for detailed explanation. new Thread(new Runnable() { @Override public void run() { try { mCurrentState = STATE_PREPARING; mPlayer.setDataSource(mSource, mVideoTrackIndex, mAudioTrackIndex); if(mPlayer == null) { // player has been release while the data source was set return; } // Async prepare spawns another thread inside this thread which really isn't // necessary; we call this method anyway because of the events it triggers // when it fails, and to stay in sync which the Android VideoView that does // the same. mPlayer.prepareAsync(); Log.d(TAG, "video opened"); } catch (IOException e) { Log.e(TAG, "video open failed", e); // Send message to the handler that an error occurred // (we don't need a message id as the handler only handles this single message) exceptionHandler.sendEmptyMessage(0); } catch (NullPointerException e) { Log.e(TAG, "player released while preparing", e); } } }).start(); } /** * Resizes the video view according to the video size to keep aspect ratio. * Code copied from {@link android.widget.VideoView#onMeasure(int, int)}. */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //Log.i("@@@@", "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", " // + MeasureSpec.toString(heightMeasureSpec) + ")"); int width = getDefaultSize(mVideoWidth, widthMeasureSpec); int height = getDefaultSize(mVideoHeight, heightMeasureSpec); if (mVideoWidth > 0 && mVideoHeight > 0) { int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { // the size is fixed width = widthSpecSize; height = heightSpecSize; // for compatibility, we adjust size based on aspect ratio if ( mVideoWidth * height < width * mVideoHeight ) { //Log.i("@@@", "image too wide, correcting"); width = height * mVideoWidth / mVideoHeight; } else if ( mVideoWidth * height > width * mVideoHeight ) { //Log.i("@@@", "image too tall, correcting"); height = width * mVideoHeight / mVideoWidth; } } else if (widthSpecMode == MeasureSpec.EXACTLY) { // only the width is fixed, adjust the height to match aspect ratio if possible width = widthSpecSize; height = width * mVideoHeight / mVideoWidth; if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { // couldn't match aspect ratio within the constraints height = heightSpecSize; } } else if (heightSpecMode == MeasureSpec.EXACTLY) { // only the height is fixed, adjust the width to match aspect ratio if possible height = heightSpecSize; width = height * mVideoWidth / mVideoHeight; if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { // couldn't match aspect ratio within the constraints width = widthSpecSize; } } else { // neither the width nor the height are fixed, try to use actual video size width = mVideoWidth; height = mVideoHeight; if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { // too tall, decrease both width and height height = heightSpecSize; width = height * mVideoWidth / mVideoHeight; } if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { // too wide, decrease both width and height width = widthSpecSize; height = width * mVideoHeight / mVideoWidth; } } } else { // no size yet, just adopt the given spec sizes } setMeasuredDimension(width, height); } private void release() { if(mPlayer != null) { mPlayer.release(); mPlayer = null; } mCurrentState = STATE_IDLE; mTargetState = STATE_IDLE; } public void setOnPreparedListener(MediaPlayer.OnPreparedListener l) { this.mOnPreparedListener = l; } public void setOnSeekListener(MediaPlayer.OnSeekListener l) { this.mOnSeekListener = l; } public void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener l) { this.mOnSeekCompleteListener = l; } public void setOnCompletionListener(MediaPlayer.OnCompletionListener l) { this.mOnCompletionListener = l; } public void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener l) { this.mOnBufferingUpdateListener = l; } public void setOnErrorListener(MediaPlayer.OnErrorListener l) { this.mOnErrorListener = l; } public void setOnInfoListener(MediaPlayer.OnInfoListener l) { this.mOnInfoListener = l; } @Override public void start() { if(isInPlaybackState()) { mPlayer.start(); } else { mTargetState = STATE_PLAYING; } } @Override public void pause() { if(isInPlaybackState()) { mPlayer.pause(); } mTargetState = STATE_PAUSED; } public void stopPlayback() { if(mPlayer != null) { mPlayer.stop(); mCurrentState = STATE_IDLE; mTargetState = STATE_IDLE; } } /** * Sets the playback speed. Can be used for fast forward and slow motion. * The speed must not be negative. * * speed 0.5 = half speed / slow motion * speed 2.0 = double speed / fast forward * speed 0.0 equals to pause * * @param speed the playback speed to set * @throws IllegalArgumentException if the speed is negative */ public void setPlaybackSpeed(float speed) { if(speed < 0) { throw new IllegalArgumentException("speed cannot be negative"); } if(isInPlaybackState()) { mPlayer.setPlaybackSpeed(speed); } mPlaybackSpeedWhenPrepared = speed; } /** * Gets the current playback speed. See {@link #setPlaybackSpeed(float)} for details. * @return the current playback speed */ public float getPlaybackSpeed() { if(isInPlaybackState()) { return mPlayer.getPlaybackSpeed(); } else { return mPlaybackSpeedWhenPrepared; } } @Override public void surfaceCreated(SurfaceHolder holder) { mSurfaceHolder = holder; openVideo(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // nothing yet } @Override public void surfaceDestroyed(SurfaceHolder holder) { mSurfaceHolder = null; release(); } @Override public int getDuration() { return mPlayer != null ? mPlayer.getDuration() : 0; } @Override public int getCurrentPosition() { if (isInPlaybackState()) { return mPlayer.getCurrentPosition(); } return 0; } @Override public void seekTo(int msec) { if(isInPlaybackState()) { mPlayer.seekTo(msec); mSeekWhenPrepared = 0; } else { mSeekWhenPrepared = msec; } } public MediaPlayer.SeekMode getSeekMode() { return mPlayer.getSeekMode(); } public void setSeekMode(MediaPlayer.SeekMode seekMode) { mPlayer.setSeekMode(seekMode); } private boolean isInPlaybackState() { return mPlayer != null && mCurrentState >= STATE_PREPARED; } @Override public boolean isPlaying() { return mPlayer != null && mPlayer.isPlaying(); } @Override public int getBufferPercentage() { return mPlayer != null ? mPlayer.getBufferPercentage() : 0; } @Override public boolean canPause() { return true; } @Override public boolean canSeekBackward() { return true; } @Override public boolean canSeekForward() { return true; } @Override public int getAudioSessionId() { return mPlayer != null ? mPlayer.getAudioSessionId() : 0; } private MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mCurrentState = STATE_PREPARED; setPlaybackSpeed(mPlaybackSpeedWhenPrepared); if(mOnPreparedListener != null) { mOnPreparedListener.onPrepared(mp); } int seekToPosition = mSeekWhenPrepared; // mSeekWhenPrepared may be changed after seekTo() call if (seekToPosition != 0) { seekTo(seekToPosition); } if(mTargetState == STATE_PLAYING) { start(); } } }; private MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener = new MediaPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { mVideoWidth = width; mVideoHeight = height; requestLayout(); } }; private MediaPlayer.OnSeekListener mSeekListener = new MediaPlayer.OnSeekListener() { @Override public void onSeek(MediaPlayer mp) { if(mOnSeekListener != null) { mOnSeekListener.onSeek(mp); } } }; private MediaPlayer.OnSeekCompleteListener mSeekCompleteListener = new MediaPlayer.OnSeekCompleteListener() { @Override public void onSeekComplete(MediaPlayer mp) { if(mOnSeekCompleteListener != null) { mOnSeekCompleteListener.onSeekComplete(mp); } } }; private MediaPlayer.OnCompletionListener mCompletionListener = new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { mCurrentState = STATE_PLAYBACK_COMPLETED; mTargetState = STATE_PLAYBACK_COMPLETED; if(mOnCompletionListener != null) { mOnCompletionListener.onCompletion(mp); } } }; private MediaPlayer.OnErrorListener mErrorListener = new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { mCurrentState = STATE_ERROR; mTargetState = STATE_ERROR; if(mOnErrorListener != null) { return mOnErrorListener.onError(mp, what, extra); } Toast.makeText(getContext(), "Cannot play the video", Toast.LENGTH_LONG).show(); return true; } }; private MediaPlayer.OnInfoListener mInfoListener = new MediaPlayer.OnInfoListener() { @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { if(mOnInfoListener != null) { return mOnInfoListener.onInfo(mp, what, extra); } return true; } }; private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener() { @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { if(mOnBufferingUpdateListener != null) { mOnBufferingUpdateListener.onBufferingUpdate(mp, percent); } } }; }